﻿
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using MahApps.Metro.Controls;
using Red.Controls.Utils;

namespace Red.Controls.Shell
{
    public partial class FrameControl : UserControl
    {
        public FrameControl()
        {
            InitializeComponent();
        }
    }

    /// <summary>
    /// Interaction logic for Frame.xaml
    /// </summary>
    [ContentProperty( "Content" )]
    public class Frame : ElementFacade<FrameControl>
    {
        #region Data

        private Window _overlayWindow;
        private Panel _overlayPanel;
        private bool _updateToolbarScheduled = false;

        #endregion Data

        #region Properties

        public static readonly DependencyProperty ToolbarProperty =
            DependencyProperty.Register( "Toolbar", typeof( ObservableCollection<UIElement> ), typeof( Frame ),
                new FrameworkPropertyMetadata( null ) );

        public new static readonly DependencyProperty ContentProperty =
            DependencyProperty.Register( "Content", typeof( ObservableCollection<Drawer> ), typeof( Frame ),
                new FrameworkPropertyMetadata( null ), IsContentValid );

        public static readonly DependencyProperty TopElementProperty =
            DependencyProperty.Register( "TopElement", typeof( FrameworkElement ), typeof( Frame ),
                new FrameworkPropertyMetadata( null, OnTopElementPropertyChanged ) );

        public static readonly DependencyProperty MainElementProperty =
            DependencyProperty.Register( "MainElement", typeof( FrameworkElement ), typeof( Frame ), 
                new FrameworkPropertyMetadata( null, OnMainElementPropertyChanged ) );

        /// <summary>
        ///     Main application toolbar. It's hidden until there's is something added to it.
        /// </summary>
        public ObservableCollection<UIElement> Toolbar
        {
            get { return (ObservableCollection<UIElement>)GetValue( ToolbarProperty ); }
            set { SetValue( ToolbarProperty, value ); }
        }

        /// <summary>
        ///     List of included drawers. Can contain up to three drawers, each with a different 'Dock' value.
        /// </summary>
        public new ObservableCollection<Drawer> Content
        {
            get { return (ObservableCollection<Drawer>)GetValue( ContentProperty ); }
            set { SetValue( ContentProperty, value ); }
        }

        /// <summary>
        ///     Placed over the MainElement and docked side drawers, under the Toolbar
        /// </summary>
        public FrameworkElement TopElement
        {
            get { return (FrameworkElement)GetValue( TopElementProperty ); }
            set { SetValue( TopElementProperty, value ); }
        }

        /// <summary>
        ///     Central element filling all the space left
        /// </summary>
        public FrameworkElement MainElement
        {
            get { return (FrameworkElement)GetValue( MainElementProperty ); }
            set { SetValue( MainElementProperty, value ); }
        }

        /// <summary>
        ///     A preview (if any) that is put inside the frame
        /// </summary>
        public EnginePreviewPanel PreviewPanel { get; private set; }

        #endregion Properties

        #region C-tor

        public Frame()
        {
            Content = new ObservableCollection<Drawer>(); // WARNING: This CANNOT be put as a property default value
            Content.CollectionChanged += Drawers_CollectionChanged;
            Toolbar = new ObservableCollection<UIElement>();
            Toolbar.CollectionChanged += Toolbar_CollectionChanged;

            Loaded += Frame_Loaded;
            Dispatcher.Hooks.DispatcherInactive += Hooks_DispatcherInactive; // "on idle"

            // Invisible, overlay window keeping the panel that is a host for temporarily-unfolded folding drawers.
            // Having a separate overlay window is required for drawers not be overlapped by other overlay windows,
            // like this used for the preview.
            _overlayPanel = new Grid();
            _overlayWindow = new Window()
            {
                Background = new SolidColorBrush() { Opacity = 1.0 },
                AllowsTransparency = true,
                WindowStyle = WindowStyle.None,
                Content = _overlayPanel,
                Focusable = false,
                ShowActivated = false,
                ShowInTaskbar = false,
                BorderThickness = new Thickness( 0 )
            };
            _overlayWindow.SetBinding( Window.DataContextProperty, new Binding( "DataContext" ) { Source = this } );
        }

        #endregion C-tor

        #region Internal methods

        /// <summary>
        ///     Restores the layout obtained by GetLayoutString()
        /// </summary>
        internal bool ApplyLayoutString( string layout )
        {
            if ( String.IsNullOrEmpty( layout ) )
            {
                return true; // keep existing layout, nothing to do
            }

            string[] drawerLayouts = layout.Split( new [] { ';' } );
            if ( drawerLayouts.Length < 3 )
            {
                // error in data, just keep existing layout
                return false; 
            }

            var left   = GetDrawerForLocation( Dock.Left );
            var right  = GetDrawerForLocation( Dock.Right );
            var bottom = GetDrawerForLocation( Dock.Bottom );

            bool success = true;
            if ( left != null ) success = left.ApplyLayoutString( drawerLayouts[ 0 ] );
            if ( right != null ) success = right.ApplyLayoutString( drawerLayouts[ 1 ] ) && success;
            if ( bottom != null ) success = bottom.ApplyLayoutString( drawerLayouts[ 2 ] ) && success;

            return success;
        }

        /// <summary>
        ///     Returns the string layout description than can be used to restore layout with ApplyLayoutString()
        /// </summary>
        internal string GetLayoutString()
        {
            var left   = GetDrawerForLocation( Dock.Left );
            var right  = GetDrawerForLocation( Dock.Right );
            var bottom = GetDrawerForLocation( Dock.Bottom );

            return String.Join( ";", new []{
                left != null   ? left.GetLayoutString() : "",
                right != null  ? right.GetLayoutString() : "",
                bottom != null ? bottom.GetLayoutString() : ""
            } );
        }

        /// <summary>
        ///     Resets the layout to the designed state
        /// </summary>
        internal void ResetLayout()
        {
            var left = GetDrawerForLocation( Dock.Left );
            var right = GetDrawerForLocation( Dock.Right );
            var bottom = GetDrawerForLocation( Dock.Bottom );

            if ( left != null ) left.ResetLayout();
            if ( right != null ) right.ResetLayout();
            if ( bottom != null ) bottom.ResetLayout();
        }

        #endregion Public methods

        #region Private methods

        private Panel GetPanelForLocation( Dock location )
        {
            switch ( location )
            {
                case Dock.Left:
                    return Element.LeftContainer;
                case Dock.Right:
                    return Element.RightContainer;
                case Dock.Bottom:
                    return Element.BottomContainer;
            }
            return null;
        }

        private GridSplitter GetSplitterForLocation( Dock location )
        {
            switch ( location )
            {
                case Dock.Left:
                    return Element.LeftSplitter;
                case Dock.Right:
                    return Element.RightSplitter;
                case Dock.Bottom:
                    return Element.BottomSplitter;
            }
            return null;
        }

        private Drawer GetDrawerForLocation( Dock location )
        {
            return Content.FirstOrDefault( drawer => drawer.Dock == location );
        }

        private static bool IsContentValid( object value )
        {
            var drawers = value as ObservableCollection<Drawer>;

            if ( drawers == null )
                return true;

            bool[] used = new bool[4];
            foreach ( Drawer drawer in drawers )
            {
                if ( used[ (int)drawer.Dock ] )
                {
                    return false; // already occupied
                }
                used[ (int)drawer.Dock ] = true;
            }
            return true;
        }

        private void UpdateOverlayPlacement( Window parent )
        {
            var dockSiteScreenPos = Element.OverlayGrid.PointToScreen( new Point( 0, 0 ) );
            _overlayWindow.Left = dockSiteScreenPos.X; // note 1px of margin around to not cover the window border
            _overlayWindow.Top = dockSiteScreenPos.Y;
            _overlayWindow.Width = Element.OverlayGrid.ActualWidth;
            _overlayWindow.Height = Element.OverlayGrid.ActualHeight;
        }

        #endregion Private methods

        #region Event handlers

        void Hooks_DispatcherInactive( object sender, EventArgs e )
        {
            if ( _updateToolbarScheduled )
            {
                if ( Element.MainToolbar.Items.Count > 0 )
                {
                    Helpers.CleanupSeparators( Element.MainToolbar.Items );
                    Element.MainToolbarTray.Visibility = Visibility.Visible;
                }

                _updateToolbarScheduled = false;
            }
        }

        private void Frame_Loaded( object sender, RoutedEventArgs e )
        {
            Window topWnd = Window.GetWindow( this );
            if ( topWnd == null )
            { // can be null on design time
                return;
            }

            topWnd.SizeChanged += TopWnd_LayoutUpdated;
            topWnd.LocationChanged += TopWnd_LayoutUpdated;
            _overlayWindow.Owner = topWnd;
            _overlayWindow.Show();
            UpdateOverlayPlacement( topWnd );
            
            RedMainWindow redWnd = topWnd as RedMainWindow;
            if ( redWnd != null )
            {
                redWnd.FlyoutsStatusChanged += RedMainWindow_FlyoutsStatusChanged;
            }
        }

        private void RedMainWindow_FlyoutsStatusChanged( object sender, RoutedEventArgs e )
        {
            var args = e as MahApps.Metro.Controls.MetroWindow.FlyoutStatusChangedRoutedEventArgs;
            if ( args.ChangedFlyout.IsVisible == false )
            {
                _overlayWindow.Visibility = Visibility.Collapsed;
            }
            else
            {
                RoutedEventHandler completionHandler = null;
                completionHandler = ( s, a ) =>
                {
                    args.ChangedFlyout.ClosingFinished -= completionHandler;
                    _overlayWindow.Visibility = Visibility.Visible;
                };
                args.ChangedFlyout.ClosingFinished += completionHandler;
            }
        }

        private void Drawers_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            switch ( e.Action )
            {
                case NotifyCollectionChangedAction.Add:
                    foreach ( object newItem in e.NewItems )
                    {
                        Drawer drawer = (Drawer)newItem;
                        drawer.RelocationRequest += OnRelocationRequest;
                        drawer.PreviewMouseDown += Element_PreviewMouseDown;
                    }
                    break;
            }
        }

        private void Element_PreviewMouseDown( object sender, MouseButtonEventArgs e )
        {
            foreach ( Drawer drawer in Content )
            {
                if ( !sender.Equals( drawer ) && drawer.DockedMode == Drawer.Mode.Folding )
                {
                    drawer.IsFolded = true;
                }
            }
        }

        private void Toolbar_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            switch ( e.Action )
            {
                case NotifyCollectionChangedAction.Add:
                    {
                        int idx = e.NewStartingIndex;
                        foreach ( object newItem in e.NewItems )
                        {
                             var items = Element.MainToolbar.Items;
                            if ( items.Count == 0 )
                            {   // Turn from collapsed to hidden on adding the first element, just for take up the space for now.
                                // It will be shown fully on scheduled update.
                                Element.MainToolbarTray.Visibility = Visibility.Hidden;
                            }
                            var element = newItem as UIElement;
                            Debug.Assert( element != null );
                            items.Insert( idx++, element );
                            element.IsVisibleChanged += ( _s, _e ) => { _updateToolbarScheduled = true; };
                        }
                    }
                    _updateToolbarScheduled = true;
                    break;
            }
        }

        private void TopWnd_LayoutUpdated( object sender, EventArgs e )
        {
            if ( sender != null )
            {
                UpdateOverlayPlacement( (Window)sender );
            }
        }

        private static void OnTopElementPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            Frame frame = (Frame)d;
            
            if ( e.OldValue != null )
                frame.Element.TopContainer.Children.Remove( (FrameworkElement)e.OldValue );
            
            if ( e.NewValue != null )
                frame.Element.TopContainer.Children.Add( (FrameworkElement)e.NewValue );
        }

        private static void OnMainElementPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            Frame frame = (Frame)d;
            
            if ( e.OldValue != null )
                frame.Element.CenterContainer.Children.Remove( (FrameworkElement)e.OldValue );

            if ( e.NewValue != null )
            {
                FrameworkElement newElem = (FrameworkElement)e.NewValue;
                frame.Element.CenterContainer.Children.Add( newElem );
                newElem.PreviewMouseDown += frame.Element_PreviewMouseDown;

                // if the central element is a preview - store it aside
                // TODO: we'll need probably multiple previews inside the frame
                // TODO: it's rather a back-compatibility solution, we need a separate property for previews
                if ( newElem is EnginePreviewPanel )
                    frame.PreviewPanel = (EnginePreviewPanel)newElem;
            }
        }

        private void OnRelocationRequest( Drawer sender, Drawer.RelocationEventArgs args )
        {
            args.DestinationPanel = GetPanelForLocation( args.RequestedLocation );
            args.DestinationSplitter = GetSplitterForLocation( args.RequestedLocation );
            args.OverlayPanel = _overlayPanel;
            args.DockGrid = Element.DockGrid;
        }

        #endregion
    }
}
